iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 13
0
自我挑戰組

從零開始的Flutter世界系列 第 13

Day13 Onboarding、Login、Sign Up (一)

  • 分享至 

  • xImage
  •  

目標:想要完成一個有Onboarding 頁,簡述此App 功能及介紹,之後進入登入頁面,並包含有註冊功能,主題為旅遊的一個 App

Basic Widget

下面先簡單介紹幾個今天會用到的 Widget內容,之後會在我們的範例專案上使用:

Route And Navigator

一個頁面,在Flutter裡面,被理解為一個Route,多個route,可以存在與同一個dart文件中,可以通過Navigator實現頁面之間的跳轉

  • 第一頁的route 通常是 ;如果是 ,initialRoute 則不需要另外設定,因為 Flutter 會自動尋找 當作 home

  • 若home 與 initialRoute 同時存在,優先去 initialRoute 頁面,若 initialRoute 為一個不存在的頁面,會去 home 頁

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: SecondWidget(),
      initialRoute: '/third',
      routes: {
        '/first': (_) =>FirstWidget(),
        '/second':(_) =>SecondWidget(),
        '/third':(_) =>ThirdWidget(),
      },
    );
  }
}
//MyApp() 會先開啟ThirdWidget 頁
  • Navigator 可以先理解成 :

    pop:回到上一頁

    push:進入下一頁

    範例:

    Navigator.pushNamed(context, '/first');
    //去 route 為/first 的頁面
    

SafeArea

現在手機有各式各樣的螢幕,像是瀏海等等,這時候我們在設計畫面時,可能會發生一些畫面被遮住的情況,此時SafeArea就能幫我們解決問題,SafeArea通過MediaQuery來檢測螢幕尺寸,使應用程式的大小能與螢幕做調配

SizedBox

常用的一個控制項,設置具體尺寸

SizedBox佈局行為相對較簡單:

  • child 不為null時,如果設置了寬高,則會強制把child尺寸調到此寬高;如果沒有設置寬高,則會根據child尺寸進行調整

  • child 為null時,如果設置了寬高,則自身尺寸調整到此寬高值,如果沒設置,則尺寸為0

Column、Row

Row:水平展示多個子控制元件的Widget

Column:垂直展示多個子控制元件的Widget

對齊方式:

  • 主軸方向的對齊方式 mainAxisAlignment

    MainAxis是主軸,就是與當前控制元件方向平行,Row 主軸為水平,Column 主軸為垂直

    MainAxisAlignment.start:將子控制元件放在最靠近主軸起點的位置

    MainAxisAlignment.end:將子控制元件放在最靠近主軸末端的位置

    MainAxisAlignment.center:將子控制元件放在最靠近主軸中間的位置

    MainAxisAlignment.spaceBetween:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離是0

    MainAxisAlignment.spaceAround:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離為子控制元件距離的一半,即d/2

    MainAxisAlignment.spaceEvenly:子控制元件之間均勻分佈,間距為d;但是第一個和最後一個控制元件距離邊界的距離也是d

  • 副軸方向的對齊方式 crossAxisAlignment

    CrossAxis是交叉軸,就是與當前控制元件方向垂直的軸,Row 副軸為垂直,Column 副軸為水平

    CrossAxisAlignment.start:將子控制元件的起始邊與crossAxis的起始邊對齊

    CrossAxisAlignment.end:將子控制元件對齊於crossAxis的末端

    CrossAxisAlignment.center:將子控制元件的起始邊與crossAxis的中間對齊

    CrossAxisAlignment.stretch:子控制元件延伸至佔滿crossAxis

    CrossAxisAlignment.baseline:將放置在螢幕上的子控制元件,對齊 baseLine (必須將TextBaseline Class與CrossAxisAlignment.baseline一起使用)

Expanded

是用於擴展RowColumn以及Flex 的子控件,Expanded 會先讓同級的其他 Widget 先佈局,之後剩餘的空間,Expanded 才會去佔用,並依Expanded 的flex 係數自動伸縮

PageView

可以實現在一個widget 裡做頁面切換的效果,我們主要以PageView.builder 介绍一下PageView 的使用

PageView.builder 透過建構參數設定畫面:

  • itemBuilder:每個頁面顯示的widget
  • itemCount:頁面數量
  • onPageChanged:頁面切換會觸發的事件,可在此時更新現在位於哪個頁面
  • controller:控制器,例如:控制跳至某一頁、控制PageView初始頁面等

AnimatedContainer

為一個Widget元件並可以在尺寸、位置、透明度等方面,實現一些動畫效果

對AnimatedContainer 想要做動畫的屬性做狀態條件判斷來設定起始樣式和結束的樣式,並設定動畫時間,當AnimatedContainer 狀態條件改變時、自身屬性變化時,會自動切換樣式並計算顯示起始與結束之間的過渡動畫

常用Button

  1. RaisedButton:有陰影,圓角
  2. FlatButton:沒有陰影 沒有圓角 沒有邊框 ,背景透明
  3. OutlineButton:沒有陰影 , 有圓角邊框
  4. IconButton:有Icon 的按鈕

按鈕屬性:onPressed,點擊按鈕的事件

Color

文件

在flutter中,color使用的是ARGB,0x後面的就是ARGB,A就是FF表示透明度,RGB就是三原色了

例如:Flutter 的logo,為完全不透明,red部分值是0x42 (66),gree部分值是0xA5 (165),blue部分值是0xF5 (245),在顏色值常用的hash syntax 語法中可寫為 #42A5F5

範例:

Color c = const Color(0xFF42A5F5); //16進位的ARGB
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5);
Color c = const Color.fromARGB(255, 66, 165, 245);
Color c = const Color.fromRGBO(66, 165, 245, 1.0); //opacity:不透明度

如果在渲染時沒有顏色出來,檢查一下顏色色碼值是8位16進制還是6位16進制,如果是6位的16進制,會預設在前面補兩個0,這樣這個顏色會是完全透明

Color c1 = const Color(0xFFFFFF); // fully transparent white (invisible)  完全透明
Color c2 = const Color(0xFFFFFFFF); // fully opaque white (visible) 完全不透明

Onboarding 頁構想:有三個分頁的畫面並可以左右滑動來敘述我們app 的特色,下方皆有跳過按鈕,讓使用者可以直接去登入頁

首先我們創建一個新的專案,我們去圖片下載,載三個圖片供我們onBoarding 的分頁用,在專案上新建一個資料夾assets,未來我們專案要使用到的資源就存在裡面,然後再在assets裡新建一個資料夾images 來給我們放圖片,之後我們把剛剛載的圖命名成splash_1、2、3,並在pubspec內去配置

配置 assets/images/ 未指定圖片的話,即該路徑的圖片皆可使用

https://ithelp.ithome.com.tw/upload/images/20200925/201184790CuoBOG95T.png

我們先規劃onBoarding 的畫面,在 lib 資料夾下建立screens資料夾,用來放之後要做的畫面,再在screens下建立splash資料夾,當作我們放onBoarding 畫面的地方,建一個splash_screen.dart用來設計我們的onBoarding 畫面,再在splash下建立components資料夾,當作我們放onBoarding 畫面裡元件的地方,我們建一個body.dart來處理我們onBoarding 畫面的body、splash_content.dart來處理我們PageView.builder 每個頁面要顯示的widget

https://ithelp.ithome.com.tw/upload/images/20200925/20118479rmhODppPWb.png

接著我們來建立一些共用的資料,共用的方法,像是app 的主要顏色,尺寸大小轉換等等,在 lib 資料夾下建立components資料夾,用來放共用元件的widget,我們建一個default_button.dart來設計共用的按鈕widget,之後我們回到lib 資料夾下,並建立constants.dart來放一些共用的屬性( app 主要顏色、文字顏色 )、routes.dart來放我們之後專案內所有頁面的routesize_config.dart來處理我們尺寸轉換等方法

https://ithelp.ithome.com.tw/upload/images/20200925/20118479tl2kOVjCiG.png

constants.dart

import 'package:flutter/material.dart';

const kPrimaryColor = Color(0xFF3E4067); 
const kPrimaryLightColor = Color(0xFF3E5067);
const kTextColor = Color(0xFF757575);

const kAnimationDuration = Duration(milliseconds: 200);

size_config.dart

import 'package:flutter/material.dart';

class SizeConfig {
  static MediaQueryData _mediaQueryData;
  static double screenWidth;
  static double screenHeight;
  static double defaultSize;
  static Orientation orientation;

  void init(BuildContext context) {
    _mediaQueryData = MediaQuery.of(context);
    screenWidth = _mediaQueryData.size.width;
    screenHeight = _mediaQueryData.size.height;
    orientation = _mediaQueryData.orientation;
  }
}

// Get the proportionate height as per screen size
double getProportionateScreenHeight(double inputHeight) {
  double screenHeight = SizeConfig.screenHeight;
  // 815 is the layout height that designer use
  return (inputHeight / 815.0) * screenHeight;
}

// Get the proportionate height as per screen size
double getProportionateScreenWidth(double inputWidth) {
  double screenWidth = SizeConfig.screenWidth;
  // 414 is the layout width that designer use 
  return (inputWidth / 414.0) * screenWidth;
}

// For add free space vertically (間距)
class VerticalSpacing extends StatelessWidget {
  const VerticalSpacing({
    Key key,
    this.of = 25,
  }) : super(key: key);

  final double of;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: getProportionateScreenHeight(of),
    );
  }
}

default_button.dart

import 'package:flutter/material.dart';

import '../constants.dart';
import '../size_config.dart';

class DefaultButton extends StatelessWidget {
  const DefaultButton({ // button onPressed 的方法透過建構傳入
    Key key,
    this.text,
    this.press,
  }) : super(key: key);
  final String text;
  final Function press;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity, //to be as big as my parent allows (double.infinity)
      height: getProportionateScreenHeight(56),
      child: FlatButton(
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), //圓角
        color: kPrimaryColor,
        onPressed: press,
        child: Text(
          text,
          style: TextStyle(
            fontSize: getProportionateScreenWidth(18),
            color: Colors.white,
          ),
        ),
      ),
    );
  }
}

splash_content.dart

import 'package:flutter/material.dart';

import '../../../constants.dart';
import '../../../size_config.dart';

class SplashContent extends StatelessWidget {
  const SplashContent({
    Key key,
    this.text,
    this.image,
  }) : super(key: key);
  final String text, image;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        VerticalSpacing(of: 25),
        Text(
          "Travel Note",
          style: TextStyle(
            fontSize: getProportionateScreenWidth(36),
            color: kPrimaryColor,
            fontWeight: FontWeight.bold,
          ),
        ),
        VerticalSpacing(of: 16),
        Padding(
          padding:
              EdgeInsets.symmetric(horizontal: getProportionateScreenWidth(20)),
          child: Text(
            text,
            textAlign: TextAlign.left,
            style: TextStyle(
              color: kTextColor,
              height: 1.5,
              fontSize: getProportionateScreenWidth(16),
            ),
          ),
        ),
        VerticalSpacing(of: 40),
        Image.asset(
          image,
          height: getProportionateScreenHeight(400),
          width: double.infinity,
        ),
      ],
    );
  }
}

body.dart

import 'package:flutter/material.dart';
import 'package:travel_note/components/default_button.dart';
import 'package:travel_note/screens/login/login_screen.dart';
import 'package:travel_note/screens/splash/components/splash_content.dart';

import '../../../constants.dart';
import '../../../size_config.dart';

class Body extends StatefulWidget {
  @override
  _BodyState createState() => _BodyState();
}

class _BodyState extends State<Body> {
  int currentPage = 0;
  List<Map<String, String>> splashData = [
    {
      "text": "Welcome to Travel Note, Let’s plan a travel!",
      "image": "assets/images/splash_1.jpg"
    },
    {
      "text": "We show the easy way to plan travel.",
      "image": "assets/images/splash_2.jpg"
    },
    {
      "text": "Just start traveling with us!",
      "image": "assets/images/splash_3.jpg"
    },
  ];

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SizedBox(
        width: double.infinity,
        child: Column(
          children: <Widget>[
            Expanded(
              child: PageView.builder(
                onPageChanged: (value) {
                  setState(() {
                    currentPage = value;
                  });
                },
                itemCount: splashData.length,
                itemBuilder: (context, index) => SplashContent(
                  image: splashData[index]["image"],
                  text: splashData[index]['text'],
                ),
              ),
            ),
            Padding(
              padding: EdgeInsets.fromLTRB(
                  getProportionateScreenWidth(25),
                  getProportionateScreenWidth(25),
                  getProportionateScreenWidth(25),
                  getProportionateScreenWidth(40)),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.end,
                children: <Widget>[
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: List.generate(
                      splashData.length,
                      (index) => buildDot(index: index),
                    ),
                  ),
                  VerticalSpacing(of: 40),
                  DefaultButton(
                    text: getButtonText(),
                    press: () {
                      Navigator.pushNamed(context, LoginScreen.routeName);
                    },
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  AnimatedContainer buildDot({int index}) {
    return AnimatedContainer(
      duration: kAnimationDuration,
      margin: EdgeInsets.only(right: 5),
      height: 6,
      width: currentPage == index ? 20 : 6,
      decoration: BoxDecoration(
        color: currentPage == index ? kPrimaryColor : Color(0xFFD8D8D8),
        borderRadius: BorderRadius.circular(3),
      ),
    );
  }

  String getButtonText() {
    if (currentPage == splashData.length - 1) {
      return "Continue";
    } else {
      return "Skip";
    }
  }
}

splash_screen.dart

import 'package:flutter/material.dart';

import '../../size_config.dart';
import 'components/body.dart';

class SplashScreen extends StatelessWidget {
  static String routeName = "/splash"; //設定onBoarding 頁的routeName
  @override
  Widget build(BuildContext context) {
    // You have to call it on your starting screen
    SizeConfig().init(context);
    return Scaffold(
      body: Body(), //使用body.dart 的Body()
    );
  }
}

routes.dart

import 'package:flutter/material.dart';
import 'package:travel_note/screens/splash/splash_screen.dart';

final Map<String, WidgetBuilder> routes = {
  SplashScreen.routeName: (context) => SplashScreen() //route 為onBoarding,就開啟onBoarding 頁
};

main.dart

import 'package:flutter/material.dart';
import 'package:travel_note/routes.dart';
import 'package:travel_note/screens/splash/splash_screen.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //拿掉畫面右上角的debug
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      /*
      當底下的頁面有很多的時候,需要在 MaterialApp 中定義Routes 並且
      同時設定 initialRoute,這樣進入 App 的時候,就會先進入 initRoutes,
      再利用 Navigator 切換不同的頁面(Route)
      initialRoute 是啓動APP的初始頁面,也就是用戶看到的第一個頁面
      */
      initialRoute: SplashScreen.routeName,
      routes: routes,
    );
  }
}

成果:

Yes

一開始設計畫面慢慢來沒關係,之後會慢慢發現設計畫面的動作都很像,並且我們越做越多共用的widget,記得自己動手做,很快就會熟練的,下一篇我們來處理登入頁


上一篇
Day12 第一支 Flutter App
下一篇
Day14 Onboarding、Login、Sign Up (二)
系列文
從零開始的Flutter世界30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言